Adding Roving Tabindex to the Date Picker
On this page
Desired Date Picker Interaction Pattern
In the last lesson, we looked at how different tabindex attribute values affect user interactivity with our page.
The current date picker implementation has a button for every day of the month, each with a tabindex of 0. This means that every button is focusable and reachable with the Tab key and the user would have to hit Tab many times in order to move on to different parts of the page.
The interaction we want is for the date picker to utilize roving tabindex.
It should only have only a single tab stop.
If the user Tabs into the date picker, they will use the Arrow keys to move around the day buttons. When the user Tabs again, they will move on to the next focusable element outside of the date picker control.
To achieve the desired result, we are going to use React’s state management to dynamically update the tabindex value of the day buttons and add Arrow key handling for moving between them.
Updating the Date Picker to Track tabindex
The first step in implementing roving tabindex is to add a state variable that will keep track of the date button’s tabIndex.
Conditionally Set tabIndex
Back at line 189 of date-picker.js where we added the hardcoded tabIndex value as a string, we’re instead going to replace it with an expression that will update it conditionally.
The value of tabIndex will be 0 if the currently focused date is the same as the date of the button we’re currently on. Otherwise, the tabIndex will be set to -1.
Here’s what the conditional looks like, using a ternary operator:
tabIndex={focusedDate === day.date ? '0' : '-1'}
We’ll create that focusedDate state variable in a moment.
Near line 175 of date-picker.js where the days of the week are mapped over, here’s what the returned button looks like now:
<button
aria-disabled={day.isBooked ? 'true' : 'false'}
aria-label={} // either the number or "already booked" or "selected"
aria-selected={isDaySelected(day) ? 'true' : 'false'}
className={...}
onClick={() => selectDay(day)}
tabIndex={focusedDate === day.date ? '0' : '-1'}
>Tracking the Focused Date in State
We need to keep track of which date the user has focused in the DatePicker component. React or similar libraries make this so much easier than handling every bit of logic manually!
Scrolling up toward the top of the component, we’ll set up the logic for tracking focusedDate in state.
Get the First Date in the Calendar
Our initial state for the focusedDate is going to be whatever the first date is in the calendar. We can get it from the first item in the datesArray that has already been created in the component.
Here’s the line of code that grabs that first date:
const firstFocusedItemDate = datesArray[0]Store firstFocusedItemDate in Component State
Now that we have a firstFocusedItemDate we’ll store it in component state using React’s useState hook.
This is where we’ll create the focusedDate variable we used in our button for the tabIndex
along with a setFocusedDate function to update it in a useState hook. We’ll also initialize it with our firstFocusedItemDate to set a default value.
const [focusedDate, setFocusedDate] = useState(firstFocusedItemDate)For more information on React’s useState hook, check out their docs.
Set up React refs for the Date Buttons
The useRef hook is one of the more common features you’ll use when dealing with focus management in React.
We’ll go further into it shortly. For now we’ll create a buttonRefs variable and set it to the result of passing an empty array into useRef.
Recapping What We’ve Done so Far
We have created a focusedDate variable in the component’s state that is set to the first date in the calendar. We also updated the rendered date buttons to set a tabindex value of 0 if one matches the current focusedDate, or -1 if it doesn’t.
Since you can only Tab to items that have a tabindex of 0, there will only be one date in the date picker that can be reached with the Tab key. Hitting Tab again will take the user to the next focusable element.
Here is the logic we implemented:
const firstFocusedItemDate = datesArray[0]
const [focusedDate, setFocusedDate] = useState(firstFocusedItemDate)
const buttonRefs = useRef([])And here is that button markup again:
<button
aria-disabled={day.isBooked ? 'true' : 'false'}
aria-label={} // either the number or "already booked" or "selected"
aria-selected={isDaySelected(day) ? 'true' : 'false'}
className={...}
onClick={() => selectDay(day)}
tabIndex={focusedDate === day.date ? '0' : '-1'}
>If we save our work and refresh the page, we can see that this code works as expected!
Video Transcript
What we need is a way to keep track of what date is focused. So some sort of a state variable. So this, rather than being a string of tab, index of negative one, we're going to need an expression. Something that we can compute, we can update.
And this is really where I love react. The way that things work is that I can put logic right in here in my markup. Might be different schools of thought on that, but for accessibility stuff, I absolutely adore it because I can have my state you know, I can do really powerful things to update user interfaces and be interactive and really tap them into kind of the internals of working with a JavaScript library.
So tab index here, rather than applying a string value, we're gonna add an expression. So we're going to say focused date, which we need to go and set up here in a second. If that equals data date, which will be the date of the button that we're currently on. If that matches, we will say tab index of zero.
I could probably say null if I wanted, because the button is focusable by default, but I'm going to put zero and then if it is not, if the focus date, when we go and update that, when that doesn't match the date of the button that we're on, we will say give it a tabindex of negative one. So we need to go and make this expression, do what we want now.
So I'm going to scroll up kind of back up to our logic section. So the first thing I need to do is track. What, what date am I focused on to begin with? So I'm going to say const first focused item date, and I'm going to pick the first thing out of the dates array.
So we have an array of dates. It's an array of literally just the dates, whereas DayJS. We have an entire object for every one of these dates. So there's other things like, is it booked? Is it selected all this data that comes into each individual date object for each one of those buttons in our grid to make things simple.
So we're not having to go and figure out what, what property on that object. We have an array of just dates. So that makes a little bit speedier to do some of these expressions. So we're going to say the first focused item date is the first date in this array of just strictly date strings. And then we need to set up some state in react.
So we'll say const and then create an assignment here for an array. And we will say focused date. So there's that focus date state variable that we were referencing in our tab index expression, so focus date, and then I functioned to call set focused date and that will equal use state. And we'll set it to default to this first focused item date that we just wrote for that first item in the dates array.
And we also need some refs. So I'm going to say const button refs. So for react, focus management, the ref is an API that you will use a lot. So I'm gonna say use ref and we have a collection of them. So I'm going to pass it an empty array. So we have an empty array for button refs, and we'll see how we use those in a moment.
But now we've got some of the logic going for our focus date. So I'm gonna hit save. So if we come over to our browser, we should have at least the first button be focusable and that is working so woohoo, our first little bit of code working? So this button for the first item in the dates array, which is also being used to render this whole table of dates, that button has tab index of zero, which is a little bit extra, you know, cause that button is focusable by default, but when we're changing it from zero to negative one, we can just safely flip it back and forth.
And you know, it doesn't really harm anything to bolt this tab index of zero onto a button when it's in service of this larger bit of logic, something that is a bit of an anti-pattern is if you have anchor links with that have tabindex zero. That starts to get a little weird. It's like, why does an anchor have tabindex on it, but it's kind of as an aside, not super relevant to this, but for this, we're going to flip it between tabindex of zero and negative one for these buttons.
So only one will have zero at a time. So we've got the first one.
Implement Arrow Key Handling in the Date Picker
Now that users can Tab into the date picker as one tab stop, we need to enable Arrow key navigation to make the whole thing functional again.
Writing the handleKeyUp Function
We need to write handler functions that will respond to DOM keyup events. For our implementation, we’ll work within React’s Synthetic Event system using our custom handler code.
Create a JavaScript function called handleKeyUp that accepts event as an argument.
The event object will be your first argument in any event handler function you write. It contains all sorts of useful data and methods to be familiar with. The MDN docs for event are worth bookmarking for future reference.
Inside the function, stub out a series of if/else statements that check if event.key (the string name of the key being pressed) is either ArrowRight, ArrowLeft, ArrowDown or ArrowUp. You can always use a console.log(event) statement inside the function to help figure out .key string names and other event details.
const handleKeyUp = (event) => {
if (event.key === 'ArrowRight') {
}
else if (event.key === 'ArrowLeft') {
}
else if (event.key === 'ArrowDown') {
}
else if (event.key === 'ArrowUp') {
}
}Creating Button Refs
Refs are a key React (and Vue) feature that will give us the ability to move focus to the next button using the Arrow keys. Refs give us an escape hatch from Synthetic Event and React library land into raw DOM nodes where we can make JavaScript event calls like .focus().
Using code like buttonRef.current, we could target a specific DOM element as opposed to a React object. (If you’ve ever worked with jQuery, it had a similar feel with jQuery objects vs. raw DOM nodes.)
Earlier we created a buttonRefs variable that we initialized to an empty array. I did it this way because I didn’t want to manually create variables for every button in the calendar. Managing 30+ buttonRef variables individually would be quite a chore, if you could do it at all!
Instead we’ll update the button markup to include a ref attribute that will dynamically push the current element ref (a reference to a DOM element) onto the buttonRefs array.
While we’re here, we’ll also add in the onKeyUp event handler by setting it to (event) => handleKeyUp(event)
The button markup now looks like this:
// Reacall that the buttons are created when we map over the weeks of the month
<button
aria-disabled={day.isBooked ? 'true' : 'false'}
aria-label={...}
aria-selected={...}
className={...}
data-date={...}
onClick={() => selectDay(day)}
onKeyUp={(event) => handleKeyUp(event)}
ref={elementRef => {buttonRefs.current.push(elementRef)}}
tabIndex={focusedDate === day.date ? '0' : '-1'}
>There’s a little bit of magic here with React’s refs API. Essentially what we’re doing is adding a particular DOM node to the full array of buttonRefs every time we create a button in the map function. Feel free to reference this code if you have to implement something similar in your own applications!
Writing a focusDayByIndex Function
Now that we have refs to each button in the calendar in an array, we can write a function to move focus as the user navigates with the Arrow keys through the date grid.
This focusDayByIndex function will take in the index of the item in the buttonRefs array that will be focused.
buttonRefs.current is an array of DOM node refs that we can use to access the item at the index we passed in. Because each item returns a raw DOM node, we can call .focus() from the DOM API in order to give it focus:
const focusDayByIndex(index) => {
buttonRefs.current[index].focus()
}🛠 Challenge: Write the Logic to Update the Focused Date Based on Arrow Key events.
For this challenge, your goal is to update the currently focused day depending on what Arrow key the user hits.
Fill in the logic for each of the Arrow key events. Essentially, each press of an Arrow key will move through the dates in the Array to execute roving tabindex in the date buttons. The array contains unformatted dates like this: [2022-07-01, 2022-07-02, 2022-07-03, ...]
If the user Arrows left or right, you’ll need to decrement or increment by 1 index in the array for moving a day behind or ahead. If they move up or down, you’ll need to decrement or increment by 7 places in the array for the number of days in a week.
Keep in mind that the date index has to be within the bounds of the calendar, so it won’t throw an error for a nonexistent date!
Hints:
You can get the button’s date from a data attribute by using event.target.dataset.date. This works because event.target references the element that dispatched the event, which in this case is the button inside of the calendar. The dataset.date part pulls the date of the calendar button from its data-date attribute in the markup.
You can get the index of a date in the dates array by calling datesArray.indexOf() and passing in a date.
Set the focused date by passing a date into setFocusedDate(), which will cause a re-render of the date grid including automatic updating of tabindex attribute values.
Video Transcript
What we need now are some events to respond so we can change this tabindex. So we've seen some events so far in our little modal example where, you know, we click a button and we fire off an event and it sends focus somewhere. In this case, we're going to fire off an event when we hit the arrow keys and we're going to change tab index.
We might even send focus to the next button so that we're kind of moving the user through this control. So I'm going to go back to vs code and we need to add some events. So on our buttons, let's add some event handling. So I'm going to come back down here onto our buttons so far, we've got an onclick
we also need some key events so that we can listen to right arrow keys I guess up, down left, right. That's really what we want to be listening to. So I'm going to say on key up so that the user, when you use your arrows. If we use on key up, that means that I can kind of change my mind and let go.
Whereas with on key down, it would hit right? When I first touched the key. So talking about the event loop down happens first on key up is kind of like at the end of that event loop. So if we do up events for keyboard if you kind of roll off of the key, you can sort of change your mind. So that's why I'm choosing keyup over keydown, but we're going to make this into a key event handler that we will call handle key up.
So we need to go to find handle key up. I'm gonna come scroll back up here. We do have a handle key. That's already existing that's for part of our modal dialogue stuff could probably be better named. But I'm gonna not touch that for the moment. Modal modal functionality should probably be in a separate component, but we'll leave that for now.
So we needed to find a handle key up, which at least we know has a different, different name. So this is going to be kind of a long function. So I'm going to say const handle key up that event. Object that's hopefully getting passed through our event binding. That's the first argument in any event handler the event object is magical and it's really helpful for focus management because it can give us things like if event dot key and I'll do a strict comparison here, if that equals arrow left.
So it's kind of a string name for the arrow key. Then I have a condition that matches the left arrow. Key Else. If event dot key, if that is a strict comparison to arrow down, now I have a condition for the down arrow key and so on. else if I'll go through and add the scaffolding for these arrow buttons, arrow down arrow, left, arrow down, arrow, arrow up.
We'll add them all. You could also use a switch statement if you preferred there's different conditionals that you could add kind of, depending on how you want to write your code, but we're just going to use an if/else statement here. So then we need a arrow, right, right. What do we not have yet left down upright?
And so the order of these doesn't really matter, I am going to move this so that it matches our example cause arrow right. Was coming first and then make sure we have . So it will sort of branch depending on which key it matches, we will get into each parts of these conditionals and have, we can do different things, you know, if it's the left arrow key
we want to move to the previous date and if it's up arrow key, we want to go to the previous week. So we need to do slightly different logic depending on different user input. So now we have a space that we can do that. And so we need to do a bit of checking. Like what date are we on? We have our tab index kind of the basis of it.
And we've got our set focused date state function. So that's going to come in handy where we can do a bit of checking to see what date were we on. What's the date of the button next to me or the previous week? And I can change that, that set focus date to whichever one, it should be, you know, seven days previous or one day previous.
🛠 Solution: Updating the Focused Date
For the first line inside of the handleKeyUp function, we’ll create a buttonDate variable and assign it the value of event.target.dataset.date. This will give us a starting point for our dates based on the date button element keyed upon by the user.
Then we’ll create another variable for the current date’s index in the datesArray.
let buttonDate = event.target.dataset.date
let buttonDateIndex = datesArray.indexOf(buttonDate)Now we’ll write the logic for each of the Arrow key events.
Handling the ArrowRight Key
If someone hits the right Arrow key, they are moving one day forward in the calendar.
We’ll create a nextDayNum variable and assign it to buttonDateIndex + 1 to increment one place in the array.
We also need to dome some checking to make sure that our index is within the bounds of the date array. For example, if we are on the last day of the month then buttonDateIndex + 1 will cause an error without logic to prevent it.
So we’ll add an if statement that checks whether a number is within the length of the array: nextDayNum < datesArray.length.
As long as we’re in bounds for the month, we can move forward with setting the focused date to the nextDayNum.
Here’s what the code for handing the right Arrow key looks like:
const handleKeyUp = (event) => {
let buttonDate = event.target.dataset.date
let buttonDateIndex = datesArray.indexOf(buttonDate)
// if date + 1 is within days array, go to next button
if (event.key === 'ArrowRight') {
let nextDayNum = buttonDateIndex + 1
if (nextDayNum < datesArray.length) {
setFocusedDate(datesArray[nextDayNum])
focusDayByIndex(nextDayNum)
}
}
...Video Transcript
let's start with a, let variable for let button date, and we will grab off of the event object. So the, the button that we have hit the key on will be under event.target another awesome property on the event object. So event.target the, this element that the button that we've clicked on will have a dataset.date.
That's part of the event or part of the date object that's attached to this particular element. So we've got a captured date for the button date. We're also going to grab its index so that whichever item it is within the collection that can be handy for selecting something out of an array of dates. So we'll say button, date, index.
Equals dates, array dot index of to get, figure out what, where are we in that array? So we can increment minus one or minus seven or plus one or plus seven for weeks or days. So index of button date. So we're going to go see for this button that I was on what's its index in the dates array. So that's a way that we can go and grab that and store it in a variable.
So within our event dot key, kind of these matching conditions for the various arrow keys we need to go and go do some logic. So within arrow, right? That'll be, you know, if we're going one to the right, it should be the current day plus one. So we're going to do a little bit of logic using the dates array to go and change, focus to the next button.
One, one day in advance, as long as we are within the bounds of the dates on the month. Cause if we're on May 31st or, you know, whatever is the last day in the month arrow, right? The way that the state picker is written is that, that arrow, right? Well, we certainly don't want it to throw an error. But if we needed it to like go to the next month or just not do anything, we have to handle that case.
So we got to check as the date that's plus one within the bounds of the dates, array. So let's say, let next day num we'll kind of go safely, go check what that could be equal. So next day, num is button date index plus one. So we're going to increment that index of the button that we were on and check we'll go check if that's within the bounds.
So if next day num is less than dates, array that length. That means that we're within the bounds so we can safely increment. So this is where we'll set focused date to go and change our tab index. So set focus date, and we're going to say dates, array next day, num so we've incremented that index by one and we're going to go and pluck it out of the dates array.
So that next date in the list it's within the bounds. We're going to go and set the next focus date to that, and then we need to go and actually focus on that, that button. So we're going to write another function that will call from each of these different branching parts of the code. We're going to say const focus day.
by Index. And this is where our refs are going to come in. That we, we created that that empty array of refs that's where this is going to come in. So we'll say focused by date day by index. We are going to pass it an index argument and say button refs. And actually let's go bind our refs real quick so that we, this will actually work.
So we're going to have a function. We're going to call to go move focus, but where are we going to move it to and how so we need to go bind our refs real quick. So I'm going to come back down here to our button, set it up so that once we finish all this logic, it should just work. So our buttons, this is where the magic is happening down here.
So our tabindex so far is managing whether we can focus on this stuff with the tab key, we also want to be able to programmatically move focus to this next item. So we kind of needed a little hook to be able to send focus there so that when I hit the right arrow key, I can check, can I focus on day plus one or week minus one?
Like, is that within the bounds? And if it is, I'm going to move focus there when I hit this arrow key, but I need a mechanism to actually kind of get to that button. And that's where refs come in, in react. So I could say ref equals. And we had an empty array because we have so many of these, we don't want to have to create individual ref variables for what, 37 buttons.
So we're going to do kind of a collection at once with that empty array. So we could say element ref, we're going to assign that to an expression and say button refs dot current. So button refs is the variable that we created. We'll say button refs dot current dot push, and we'll push this particular element ref.
So what this is doing is saying for this particular button that we're iterating over right now, cause this is a dynamic template. We could say the ref for this button, this element ref go and push it into our button, refs array. And it's a little bit, little bit magic. The refs API and react has this property called current.
So for each ref it's current property, we'll get you access to the actual Dom node. And so this is a bit of magic code to create a collection of refs and so we're, we're appending to the button refs array with this, you know, each item that we're iterating over. I would probably have to copy this code.
I wouldn't remember this off the top of my head, so this seems extra magic. You're not alone. But you know, once you've done these techniques a few times, you can kind of reference code, be like, wait a minute. I've worked with collections of refs before. Let me go look at that example I made. So I can jog my memory of how that worked.
So just know that yeah, many of us have to go and reference the internet and our own code before, but this is so we can work with a collection of refs. Okay. So now coming back up to our focus day by index, now we can actually make that work. So we've got button refs, that's an array, right? So we've, we've done our binding down below in our JSX so that our buttons are now when they're created, they're getting added to that button, refs array. So button refs, kind of, whether it's a single ref or a collection, like we've created them.
We get this current property because we have a collection of them. Current has an array associated, so we can go and check by index kind of we've we've been working with an array of dates. So that's like, we're kind of using the array of dates like this structure of all the dates in a list. We can use that to kind of match like dates that get put into the date grid and the order of the buttons that we've got.
So we're kind of like really leveraging this, this list of dates in this array, including with our refs. So I can kind of check for that date in that array. Give me the button that's in the same spot. And so I can access the Dom node with it in react. So button refs dot current, give me that particular button in the list and give me access to the DOM nodes so I can hit dot focus.
So when I hit that arrow key, for example, when I hit the arrow, right, I can come in here and say focus day by index. Next day num we've already validated that that's within the bounds. You know, that's our right arrow key plus one. so we've got our arrow, right
Handling the Rest of the Arrow Keys
The logic for handling the rest of the Arrow keys is fairly similar to what we did for the ArrowRight key.
For the left Arrow, we’ll make sure date - 1 is in bounds.
For the up and down Arrows, we’ll look for date - 7 and date + 7 to be in bounds.
The finalized code for handling the Arrow keys looks like this:
const handleKeyUp = (event) => {
let buttonDate = event.target.dataset.date
let buttonDateIndex = datesArray.indexOf(buttonDate)
// if date + 1 is within days array, go to next button
if (event.key === 'ArrowRight') {
let nextDayNum = buttonDateIndex + 1
if (nextDayNum < datesArray.length) {
setFocusedDate(datesArray[nextDayNum])
focusDayByIndex(nextDayNum)
}
}
// if date - 1 is within days array, go to previous button
else if (event.key === 'ArrowLeft') {
let previousDayNum = buttonDateIndex - 1
if (previousDayNum >= 0) {
setFocusedDate(datesArray[previousDayNum])
focusDayByIndex(previousDayNum)
}
}
// if date + 7 is within days array, go to next week
else if (event.key === 'ArrowDown') {
let nextWeekNum = buttonDateIndex + 7
if (datesArray[nextWeekNum]) {
setFocusedDate(datesArray[nextWeekNum])
focusDayByIndex(nextWeekNum)
}
}
// if date - 7 is within days array, go to previous week
else if (event.key === 'ArrowUp') {
let previousWeekNum = buttonDateIndex - 7
if (datesArray[previousWeekNum]) {
setFocusedDate(datesArray[previousWeekNum])
focusDayByIndex(previousWeekNum)
}
}
}Troubleshooting Arrow Key Handling
If you’ve followed along with the text so far, the code above all works.
However, there was some troubleshooting involved that you can watch in the video.
The first step I took was to verify that the key events were being recognized properly by logging out event.key in the handleKeyUp function.
When I verified that they were, I discovered there must be an issue with the button markup as the pattern wasn’t quite working yet.
It ended up that the actual buttonDate wasn’t being set, which is the way to tell which button is the active one. Updating the buttons to include the data-date attribute is ultimately what fixed everything.
Video Transcript
Let's go through and do the rest. And then we should have a working roving tab index.
So we've manipulated tabindex so far, we're going to update that tab index using this state function. And then when that runs, you know, we've changed the tab index. So that, that button we're trying to get to is going to be the focusable one we're going to send focus there. So we've got all the working so far, we just need to bind up all of our arrow keys.
So for our next one, arrow left would be the previous day, previous day num or prev day num you can kind of name them, whatever you like, but we need some sort of variable for the previous day. So it would be button date, index minus one. We need to check again if that's within the bounds. So if previous day num if that is greater than or equal to zero, because our array starts at zero, we don't really need to check the start of the array.
We know it starts at zero. So rather than up here where we were checking dates, right up length, we can just assume zero is the first spot in the array index. So for our previous day, if that's greater than or equal to zero, we know we have space to move to the previous day. So I can say if that, if that condition matches, I can say set focused date dates, array, and we'll say previous day now that index that we've qualified is within the bounds.
And from this point on, we're mostly just repeating things with slight tweaks, kind of, depending on where we're trying to get to with our arrow keys. So set the focus day by index previous day, num should be good. And then now we're in a previous and next week, so we're going up and down arrow keys.
So we could say, let for arrow down, that would be next week num right? So that would equal button date index plus seven. So we're going to do seven day weeks. So if next week num is that even in the dates, array, dates, array next week num, it's going to go check if that exists.
That's another way that we can check if it's in the bound. So that will be truthy if. Exists. And if that is the case, we can say set focused date next week, num
and again, with our focused by day index. And we'll say next week, num and one more num previous week num button date index minus seven. And we'll do a similar comparison to check if dates, array has an index for previous week num if that is truthy, truthy is such a funny word to say, isn't it set?
Focus date previous week num and our focused by day index. Got it. Okay. So now we have code for up down left. We're doing all this intricate checking, you know, are we within the bounds? And we're really using the dates for the button that we're on, kind of comparing it to items within an array. So let's refresh and go see, did we get it all?
Did it work? I'm going to hit refresh here. So we know that we had our tab index was working before we added all of this code. Let's see what happens. So I'm gonna hit tab, get onto here and I might need to refresh. We'll see I don't see anything happening, so let's go figure out what's going on. I check the console, no errors. Oh, if I hit the down arrow key, I get an issue focused J by index. Oh, I named that wrong or I called it wrong focused and I could just select multiples at once with command D. So this is focus day.
That's an action. So set or focus. So let's refresh that now. Oh, so I was able to do a down arrow. I'm still scrolling the page. So that's something that we could address, So let's go back to vs code and do some debugging cause something went wrong here.
So the first thing I could do is double check that I put everything in the right place so I can do some console logging. This is always what I would do. And it is what I did when I originally played around with this. So I could log event key. For example, I could move my console log into each of these conditionals and make sure that those all got written correctly.
So yeah, I've got to see what's going on here. So we've got arrow down. That should be next, next week. Arrow up should be previous week. We've got arrow left for previous day and arrow right for next day. That should work. Just kind of thinking through like what key for what logic that really should work, but it's not so not quite sure what's going on there.
Let's let's do a little bit of logging and make sure that our first make sure our function's getting called. I think it is because we saw something happen. So, so I see I tabbed onto this. So our handle or our key up. Is working cause it logged a tab. So when I hit tab, we can see that event dot key equal tab.
So I see arrow right arrow down, arrow, up arrow left. So all of our keys are being responded to, but there's something going on with our logic. It's not quite working. So we got to go figure out what's going on with that.
So let's take away some pieces, right? So maybe we could come in here. We know that our event keys worked, let's go and see, maybe we're missing something on our buttons.
I may have forgotten something. I think. Yes. I think I actually, I know what it is. So this dataset, if it seemed a little bit magical it's because I sort of hand waved over it and we didn't really dig into what that was. So I think we're missing something that is this button date. If button, date doesn't get set, none of this stuff will work.
So let's go down here to our buttons and double check. Do we have a dataset? So dataset will come off of a data attribute and that is what we're missing. So I've alphabetized these cause I'm a nerd like that. So in between class name and on click, I'm going to add data dash date. That's what we were missing.
So we are going to get day.date. So this button. Has a whole day JS object attached to it, but there's so much more than we need because we just want to match the date with that dates, array, like, keep it simple. It'll make it fast. You know, we're just comparing strings, not whole objects. It's a lot less work.
But we need a data attribute here for a date. data attributes are awesome because you can create arbitrary attributes. You can make them up, you can use them for testing. You can use them for functions like this, where it doesn't have to be anything that the JavaScript library knows about or that the browser knows about data attributes are awesome.
So we can add this arbitrary data.date. Now our code should work. I hope so. event.target.Dataset dataset will give you a collection of any data attributes in this case, including date. So kind of important to make our code work with that. So I'm going to come back over to the browser. We'll hit refresh again and come down here and see voila right arrow key up arrow key left.
Overriding Browser Scrolling from Up and Down Arrows
To make it so the up and down Arrows don’t scroll the page, we’ll write a keyDown handler that overwrites the default behavior of the keys.
Overriding default browser behavior is a judgement call you have to make. For our date picker, the Arrow keys are being used within the context of a particular component. Since this custom functionality is for a specific and limited use case, it does feel appropriate. Think long and hard every time you override defaults and test with users!
Writing the handleKeyDown Function
The first thing we need to is add an onKeyDown to the button markup. Similar to the onKeyUp handler we wrote, onKeyDown will call the handleKeyDown function we’re about to write.
Back up near the top of the file where we wrote our handleKeyUp function, create a new function for handleKeyDown that also takes in the event.
This time instead of writing an if-elsestatement, I’m going to write a switch statement that will look at what event.key is and act accordingly.
We can take advantage of the switch’s feature where multiple cases can run the same code. We need to prevent the default behavior of both the up and down Arrows.
When we’re done, we can break out of the switch.
Here’s what it looks like all together:
const handleKeyDown = (event) => {
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
event.preventDefault()
break
}
}Now when the up and down Arrow keys are used to navigate through the weeks, the browser scroll will no longer scroll around!
Video Transcript
So I'm still getting some scrolling in the browser that's because the arrow keys scroll the browser by default. So if we want to override that because we're in a specific context here, I mean, it's kind of subjective. Do we really want to remove that functionality? I mean, if I scroll down and it's if it's like out of the viewport, maybe I don't want to, I don't know, let's go bind it.
So you know how, and then you can kind of make the judgment call of like, do I want to disable scrolling or not? You really want to think long and hard about whether that is appropriate, because that is a default function of the arrow keys is scrolling the browser, but we're doing, we're kind of overloading the arrow keys.
We're adding custom functionality to a specific, limited use case. So let's go deal with that. So that you know how I can get this console log out of here. So we've got a handle key up. How about we go and add an handle key down and then check. If you're not using one of the arrow keys, then we will prevent the, the arrow keys or prevent the scrolling.
So we've got on key up, let's add an on key down. So we've got an independent key handler handle key down, and we will do a similar thing where it will let the event object just get passed through this event bindings, since we're not actually calling the function, we don't need to do that extra binding.
When we just reference our key handler function, it will automatically pass through the event object for us, which is cool. So I'm going to come back up here and right above our handle, key up, I'll say const handle key down there's that event object that we want, because that will tell us what key we've pressed.
All kinds of things, you know, what event.target we were on. So for our button, that's how we got access to our data attributes. The event object is a huge part of focus management. So for our handle, key down, this is, I am gonna use a different conditional here. So instead of if else let's do a switch statement, switch event dot key, kind of cool.
So you can see different methods. So instead of if or else, if I can use switch in JavaScript and say in the case of arrow up, if that string matches, you know, if I'm hitting up or down, I don't really care about left or right. So much. It's mostly just up and down that was scrolling the browser window. So if that case comes up, I can also do multiple cases in one condition and say case arrow down, because those are, you know, I want to prevent default in either one of those.
I could say event dot prevent default, call that, and then we'll break our switch statement so that it doesn't just get stuck in a loop. Switch is really cool. You can add a bunch of different cases. Like we could do a switch for arrow right, arrow left kind of independently. We could have a default case here if we wanted.
Switch can be nice. Sometimes made a nice little concise statement here instead of doing if event dot key, triple equals arrow, right. Or event dot key, triple equals arrow or arrow up arrow down. It just becomes a little bit lengthier than an if/else like having the switch available different conditionals.
So now we've got this handle key down. Let's go back to the browser, hit refresh. I'm gonna come over here. So now when we hit the up and down arrow keys, it's moving us around without scrolling the window, which just seems a bit more intentional. But we're limiting it so that, you know, I'm not doing prevent default for any key.
Cause I still want to be able to hit the enter key or the space bar. Like if I did event dot prevent default, it could have other consequences. You know, there's other functions going on. There's other keys being pressed. So I do need that conditional on our switch statement to limit this to up and down arrow keys.
And then if I tab off of here, you know, if we come onto our reserve button, for example, now the arrow keys work again, which is what we want. We want to be able to operate the browser window with our up and down arrow keys. You want to be careful when you're kind of overriding default methods because sometimes, you know, you might be taking away functionality that the browser gets for free.
For example, I saw a like a tweet from a friend. They said they saw a nightmare scenario on a website where someone had made a carousel that they, they removed the browser's back button functionality to make it operate a carousel which is huge pain like that is a hill. I would die on. If I had a marketing person be like, well, what if we made this key command operative slideshow?
Like, no, give me a different button or some other control. So, we should be careful with, especially with arrow keys, you know, maybe there's some other functionality that the, the screen reader needs to do that we want to make sure we're testing and make sure things still work. So we've got roving tab index here,